package com.xdb.sqlparser.dialect.mysql.parser;

import static com.xdb.sqlparser.parser.SQLCharTypes.isFirstIdentifierChar;
import static com.xdb.sqlparser.parser.SQLLayoutCharacters.EOI;
import static com.xdb.sqlparser.parser.SQLToken.LITERAL_CHARS;

import java.util.HashMap;
import java.util.Map;

import com.xdb.sqlparser.exception.NotAllowCommentException;
import com.xdb.sqlparser.exception.ParserException;
import com.xdb.sqlparser.parser.SQLCharTypes;
import com.xdb.sqlparser.parser.SQLKeywords;
import com.xdb.sqlparser.parser.SQLLexer;
import com.xdb.sqlparser.parser.SQLToken;

public class MySqlLexer extends SQLLexer {

	public final static SQLKeywords DEFAULT_MYSQL_KEYWORDS;

	static {
		Map<String, SQLToken> map = new HashMap<String, SQLToken>();

		map.putAll(SQLKeywords.DEFAULT_KEYWORDS.getKeywords());

		map.put("DUAL", SQLToken.DUAL);
		map.put("FALSE", SQLToken.FALSE);
		map.put("IDENTIFIED", SQLToken.IDENTIFIED);
		map.put("IF", SQLToken.IF);
		map.put("KILL", SQLToken.KILL);

		map.put("LIMIT", SQLToken.LIMIT);
		map.put("TRUE", SQLToken.TRUE);
		map.put("BINARY", SQLToken.BINARY);
		map.put("SHOW", SQLToken.SHOW);
		map.put("CACHE", SQLToken.CACHE);
		map.put("ANALYZE", SQLToken.ANALYZE);
		map.put("OPTIMIZE", SQLToken.OPTIMIZE);
		map.put("ROW", SQLToken.ROW);
		map.put("BEGIN", SQLToken.BEGIN);
		map.put("END", SQLToken.END);

		DEFAULT_MYSQL_KEYWORDS = new SQLKeywords(map);
	}

	public MySqlLexer(char[] input, int inputLength, boolean skipComment) {
		super(input, inputLength, skipComment);
		super.keywods = DEFAULT_MYSQL_KEYWORDS;
	}

	public MySqlLexer(String input) {
		super(input);
		super.keywods = DEFAULT_MYSQL_KEYWORDS;
	}

	public void scanSharp() {
		if (ch != '#') {
			throw new ParserException("illegal stat");
		}

		if (charAt(pos + 1) == '{') {
			scanVariable();
			return;
		}

		SQLToken lastToken = this.token;

		scanChar();
		mark = pos;
		bufPos = 0;
		for (;;) {
			if (ch == '\r') {
				if (charAt(pos + 1) == '\n') {
					bufPos += 2;
					scanChar();
					break;
				}
				bufPos++;
				break;
			} else if (ch == EOI) {
				break;
			}

			if (ch == '\n') {
				scanChar();
				bufPos++;
				break;
			}

			scanChar();
			bufPos++;
		}

		stringVal = subString(mark, bufPos);
		token = SQLToken.LINE_COMMENT;
		hasComment = true;

		if (commentHandler != null && commentHandler.handle(lastToken, stringVal)) {
			return;
		}

		endOfComment = isEOF();

		if (!isAllowComment() && (isEOF() || !isSafeComment(stringVal))) {
			throw new NotAllowCommentException();
		}
	}

	public void scanVariable() {
		if (ch != '@' && ch != ':' && ch != '#' && ch != '$') {
			throw new ParserException("illegal variable");
		}

		mark = pos;
		bufPos = 1;

		if (charAt(pos + 1) == '@') {
			ch = charAt(++pos);
			bufPos++;
		}

		if (charAt(pos + 1) == '`') {
			++pos;
			++bufPos;
			char ch;
			for (;;) {
				ch = charAt(++pos);

				if (ch == '`') {
					bufPos++;
					ch = charAt(++pos);
					break;
				} else if (ch == EOI) {
					throw new ParserException("illegal identifier");
				}

				bufPos++;
				continue;
			}

			this.ch = charAt(pos);

			stringVal = subString(mark, bufPos);
			token = SQLToken.VARIANT;
		} else if (charAt(pos + 1) == '{') {
			++pos;
			++bufPos;
			char ch;
			for (;;) {
				ch = charAt(++pos);

				if (ch == '}') {
					bufPos++;
					ch = charAt(++pos);
					break;
				} else if (ch == EOI) {
					throw new ParserException("illegal identifier");
				}

				bufPos++;
				continue;
			}

			this.ch = charAt(pos);

			stringVal = subString(mark, bufPos);
			token = SQLToken.VARIANT;
		} else {
			for (;;) {
				ch = charAt(++pos);

				if (!isIdentifierChar(ch)) {
					break;
				}

				bufPos++;
				continue;
			}
		}

		this.ch = charAt(pos);

		stringVal = subString(mark, bufPos);
		token = SQLToken.VARIANT;
	}

	public void scanIdentifier() {
		final char first = ch;

		if (ch == '`') {

			mark = pos;
			bufPos = 1;
			char ch;
			for (;;) {
				ch = charAt(++pos);

				if (ch == '`') {
					bufPos++;
					ch = charAt(++pos);
					break;
				} else if (ch == EOI) {
					throw new ParserException("illegal identifier");
				}

				bufPos++;
				continue;
			}

			this.ch = charAt(pos);

			stringVal = subString(mark, bufPos);
			SQLToken tok = keywods.getKeyword(stringVal);
			if (tok != null) {
				token = tok;
			} else {
				token = SQLToken.IDENTIFIER;
			}
		} else {

			final boolean firstFlag = isFirstIdentifierChar(first);
			if (!firstFlag) {
				throw new ParserException("illegal identifier");
			}

			mark = pos;
			bufPos = 1;
			char ch;
			for (;;) {
				ch = charAt(++pos);

				if (!isIdentifierChar(ch)) {
					break;
				}

				bufPos++;
				continue;
			}

			this.ch = charAt(pos);

			stringVal = addSymbol();
			SQLToken tok = keywods.getKeyword(stringVal);
			if (tok != null) {
				token = tok;
			} else {
				token = SQLToken.IDENTIFIER;
			}
		}
	}

	protected final void scanString() {
		{
			boolean hasSpecial = false;
			int startIndex = pos + 1;
			int endIndex = -1; // text.indexOf('\'', startIndex);
			for (int i = startIndex; i < text.length(); ++i) {
				final char ch = text.charAt(i);
				if (ch == '\\') {
					hasSpecial = true;
					continue;
				}
				if (ch == '\'') {
					endIndex = i;
					break;
				}
			}

			if (endIndex == -1) {
				throw new ParserException("unclosed str");
			}

			String stringVal = subString(startIndex, endIndex - startIndex);
			// hasSpecial = stringVal.indexOf('\\') != -1;

			if (!hasSpecial) {
				this.stringVal = stringVal;
				int pos = endIndex + 1;
				char ch = charAt(pos);
				if (ch != '\'') {
					this.pos = pos;
					this.ch = ch;
					token = LITERAL_CHARS;
					return;
				}
			}
		}

		mark = pos;
		boolean hasSpecial = false;
		for (;;) {
			if (isEOF()) {
				lexError("unclosed.str.lit");
				return;
			}

			ch = charAt(++pos);

			if (ch == '\\') {
				scanChar();
				if (!hasSpecial) {
					initBuff(bufPos);
					arraycopy(mark + 1, buf, 0, bufPos);
					hasSpecial = true;
				}

				switch (ch) {
					case '\0' :
						putChar(ch = '\0');
						break;
					case '\'' :
						putChar('\'');
						break;
					case '"' :
						putChar('"');
						break;
					case 'b' :
						putChar('\b');
						break;
					case 'n' :
						putChar('\n');
						break;
					case 'r' :
						putChar('\r');
						break;
					case 't' :
						putChar('\t');
						break;
					case '\\' :
						putChar('\\');
						break;
					case 'Z' :
						putChar((char) 0x1A); // ctrl + Z
						break;
					default :
						putChar(ch);
						break;
				}

				continue;
			}
			if (ch == '\'') {
				scanChar();
				if (ch != '\'') {
					token = LITERAL_CHARS;
					break;
				} else {
					if (!hasSpecial) {
						initBuff(bufPos);
						arraycopy(mark + 1, buf, 0, bufPos);
						hasSpecial = true;
					}
					putChar('\'');
					continue;
				}
			}

			if (!hasSpecial) {
				bufPos++;
				continue;
			}

			if (bufPos == buf.length) {
				putChar(ch);
			} else {
				buf[bufPos++] = ch;
			}
		}

		if (!hasSpecial) {
			stringVal = subString(mark + 1, bufPos);
		} else {
			stringVal = new String(buf, 0, bufPos);
		}
	}

	public void scanComment() {
		SQLToken lastToken = this.token;

		if (ch == '-') {
			char next_2 = charAt(pos + 2);
			if (isDigit(next_2)) {
				scanChar();
				token = SQLToken.SUB;
				return;
			}
		} else if (ch != '/') {
			throw new IllegalStateException();
		}

		mark = pos;
		bufPos = 0;
		scanChar();

		// /*+ */
		if (ch == '*') {
			scanChar();
			bufPos++;

			while (ch == ' ') {
				scanChar();
				bufPos++;
			}

			boolean isHint = false;
			int startHintSp = bufPos + 1;
			if (ch == '!') {
				isHint = true;
				scanChar();
				bufPos++;
			}

			for (;;) {
				if (ch == EOI) {
					this.token = SQLToken.ERROR;
					return;
				}
				if (ch == '*' && charAt(pos + 1) == '/') {
					bufPos += 3;
					scanChar();
					scanChar();
					break;
				}

				scanChar();
				bufPos++;
			}

			if (isHint) {
				stringVal = subString(mark + startHintSp, (bufPos - startHintSp) - 2);
				token = SQLToken.HINT;
			} else {
				stringVal = subString(mark, bufPos);
				token = SQLToken.MULTI_LINE_COMMENT;
				hasComment = true;
			}

			if (commentHandler != null && commentHandler.handle(lastToken, stringVal)) {
				return;
			}

			endOfComment = isEOF();

			if (!isHint && !isAllowComment() && !isSafeComment(stringVal)) {
				throw new NotAllowCommentException();
			}

			return;
		}
		if (ch == '/' || ch == '-') {
			scanChar();
			bufPos++;

			for (;;) {
				if (ch == '\r') {
					if (charAt(pos + 1) == '\n') {
						bufPos += 2;
						scanChar();
						break;
					}
					bufPos++;
					break;
				} else if (ch == EOI) {
					break;
				}

				if (ch == '\n') {
					scanChar();
					bufPos++;
					break;
				}

				scanChar();
				bufPos++;
			}

			stringVal = subString(mark, bufPos + 1);
			token = SQLToken.LINE_COMMENT;
			hasComment = true;

			if (commentHandler != null && commentHandler.handle(lastToken, stringVal)) {
				return;
			}

			endOfComment = isEOF();

			if (!isAllowComment() && (isEOF() || !isSafeComment(stringVal))) {
				throw new NotAllowCommentException();
			}

			return;
		}
	}

	private boolean isIdentifierChar(char c) {
		return c != '#' && SQLCharTypes.isIdentifierChar(c);
	}

	public void scanNumber() {
		mark = pos;

		if (ch == '-') {
			bufPos++;
			ch = charAt(++pos);
		}

		for (;;) {
			if (ch >= '0' && ch <= '9') {
				bufPos++;
			} else {
				break;
			}
			ch = charAt(++pos);
		}

		boolean isDouble = false;

		if (ch == '.') {
			if (charAt(pos + 1) == '.') {
				token = SQLToken.LITERAL_INT;
				return;
			}
			bufPos++;
			ch = charAt(++pos);
			isDouble = true;

			for (;;) {
				if (ch >= '0' && ch <= '9') {
					bufPos++;
				} else {
					break;
				}
				ch = charAt(++pos);
			}
		}

		if (ch == 'e' || ch == 'E') {
			bufPos++;
			ch = charAt(++pos);

			if (ch == '+' || ch == '-') {
				bufPos++;
				ch = charAt(++pos);
			}

			for (;;) {
				if (ch >= '0' && ch <= '9') {
					bufPos++;
				} else {
					break;
				}
				ch = charAt(++pos);
			}

			isDouble = true;
		}

		if (isDouble) {
			token = SQLToken.LITERAL_FLOAT;
		} else {
			if (isFirstIdentifierChar(ch) && !(ch == 'b' && bufPos == 1 && charAt(pos - 1) == '0')) {
				bufPos++;
				for (;;) {
					ch = charAt(++pos);

					if (!isIdentifierChar(ch)) {
						break;
					}

					bufPos++;
					continue;
				}

				stringVal = addSymbol();
				token = SQLToken.IDENTIFIER;
			} else {
				token = SQLToken.LITERAL_INT;
			}
		}
	}
}
